package com.xzy.seckill.service.impl;

import com.xzy.seckill.common.exception.Assert;
import com.xzy.seckill.common.result.ResponseEnum;
import com.xzy.seckill.pojo.entity.Order;
import com.xzy.seckill.pojo.entity.SeckillOrder;
import com.xzy.seckill.mapper.SeckillOrderMapper;
import com.xzy.seckill.pojo.vo.SeckillGoodDetailVO;
import com.xzy.seckill.service.SeckillOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xzy.seckill.service.mq.MQSender;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author xzy
 * @since 2022-01-27
 */
@Service
@Slf4j
public class SeckillServiceImpl extends ServiceImpl<SeckillOrderMapper, SeckillOrder> implements SeckillOrderService {



    @Autowired
    RedisTemplate redisTemplate;

    @Autowired
    MQSender mqSender;

    //内存标记 库存是否充足，用于减少redis访问

    private volatile static Map<String,Boolean> stockTag  = new HashMap<>();



    /**
     * 秒杀操作
     * @param userId 用户id
     * @param goods 商品id
     * @return 秒杀成功的订单
     *
     * 超卖问题解决
     *
     */
    @Override
    public Order doSeckill2(Integer userId, SeckillGoodDetailVO goods) {

        healthRequest(userId);
        //校验及扣库存
        boolean b = stockTicket(goods.getId().toString());
        //断言秒杀成功
        Assert.isTrue(b, ResponseEnum.FAIL_SECKILL);


        //计算预入单过期时间 单位s
        Long second =  goods.getEndDate().toEpochSecond(ZoneOffset.of("+8"))-
                LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));


        //redis预入订单
        PreOrder(userId,goods.getId(),second);


        //真正订单入库操作使用mq执行
        return order1(userId, goods);
    }




    /**
     * redis原子操作预检库存操作
     * 操作的前提是库存表已经存入redis中，（可在添加秒杀商品接口中执行）
     * 此操作执行了校验库存及扣除库存的操作
     * @return 预扣库存是否成功
     */
    private boolean stockTicket(String goodId){
        //库存数量键
        String key = "sec:product:stock:"+goodId;

        //校验是否存在此商品
        Object value = redisTemplate.opsForValue().get(key);
        if (null==value){
            //商品不存在
            log.info("商品不存在");
            return false;
        }
        //检查库存
        Integer stock = (Integer) value;
        if (stock<=0){
            //库存不足
           // log.info("预检时库存不足");
            // todo: 这里可以添加一个内存标记 减少redis访问

            stockTag.put(goodId,false);
            return  false;
        }
        //此处库存充足
        //但是不能直接进行减库存操作 ，高并发访问情况下，此时可能有其他线程已经将redis的key修改了

        //redis 预检库存操作
        Long increment = redisTemplate.opsForValue().increment(key, -1);
        //这里的 increment是原子操作的产物，所以一定是线程安全的
        if (increment>=0){
            //秒杀成功
            //todo： 这里再执行数据同步，可以使用mq的操作异步同步数据

          //  log.info("秒杀成功");
            //这里暂时直接插入
            //下面这个操作错误的原因是执行此操作并不是按顺序执行，有可能最后一次修改并不是increment=0的那条线程，
            //有可能是increment=任何数的一条线程去做的最后一次修改，导致数据库中最后数据不为0
//            seckillGoodsService.update(
//                    new UpdateWrapper<SeckillGoods>().eq("goods_id",goodId).set("stock_count",increment));


            //正确操作还是应该是库中自减
//            seckillGoodsService.update(
//                   new UpdateWrapper<SeckillGoods>().eq("goods_id",goodId).
//                           setSql("stock_count=stock_count-1"));  //这里的自减是原子操作，线程安全

            /*此处异步同步数据库*/
            mqSender.sendStockTicket(goodId);
            return true;
        }else {
            //秒杀失败，在此前第一次查看库存与第二次原子减库存之间有线程修改库存导致库存不足了
            //为了保证数据的线程安全性，需要回退数据，将之前减去的redis中库存回退
            redisTemplate.opsForValue().increment(key,1);
            return false;
        }
    }

    /**
     * 使用redis预存订单，请求刚进入时就查看预存订单表，若表中有数据则证明重复下单
     *
     * @param userId 用户id
     * @param goodsId 商品id
     * @param Time 过期时间 s（一般为秒杀结束后过期）
     * @return
     */

    private boolean PreOrder(Integer userId,Integer goodsId,Long Time){

        String key = "sec:product:order:"+userId+":"+goodsId;
        redisTemplate.opsForValue().set(key,1,Time, TimeUnit.SECONDS);
        //todo： 这里再执行真正的入单操作，可以使用mq进行异步入单

        return true;



    }

    /**
     * 校验订单
     * 校验redis中是否预存了订单信息，若有则表示重复下单
     * @param userId 用户id
     * @param goodsId 商品id
     * @return 是否重复下单，true表示未重复下单
     */
    @Override
    public boolean checkOrder(Integer userId,Integer goodsId){
        String key = "sec:product:order:"+userId+":"+goodsId;
        Object o = redisTemplate.opsForValue().get(key);
        //为空则证明第一次下单
        Assert.isTrue(null==o,ResponseEnum.REPEAT_ORDER);
        return true;
    }

    @Override
    public Map<String,Boolean> getSotckTag() {
        return this.stockTag;
    }


    /**
     * 使用redis拦截同一用户快速二次请求
     * @param userId 请求用户
     * @return 是否放行
     */
    private boolean healthRequest(Integer userId){
        String key = "sec:req:"+userId;
        long count = redisTemplate.opsForValue().increment(key, 1);
        if (count == 1) {
        //设置有效期2秒
            redisTemplate.expire(key, 2, TimeUnit.SECONDS);
        }
        if (count > 1) {
           //重复提交订单
            //抛出异常
            Assert.isTrue(false,ResponseEnum.REPEAT_req);
        }
        return true;
    }


    private Order order1(Integer userId, SeckillGoodDetailVO goods){
        //订单生成
        Order order = new Order();
        order.setUserId(userId);
        order.setGoodsName(goods.getGoodsName());
        order.setGoodsId(goods.getId());
        order.setDeliveryAddrId(0);
        order.setGoodsCount(1);
        order.setGoodsPrice(goods.getSeckillPrice());
        order.setOrderChannel(1);
        order.setStatus(0);
        mqSender.sendOrder(order);
        return order;
    }




}
